package fr.jade.fraclite.orb;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.objectweb.fractal.api.Component;
import org.objectweb.fractal.api.Interface;
import org.objectweb.fractal.api.Type;

import fr.jade.fraclite.orb.api.Name;
import fr.jade.fraclite.orb.io.RmiObjectInputStream;
import fr.jade.fraclite.orb.io.RmiObjectOutputStream;
import fr.jade.fraclite.orb.io.messages.AdminMessage;
import fr.jade.fraclite.orb.io.messages.RpcRequest;
import fr.jade.fraclite.orb.names.TcpIpName;

/**
 * A stub gives access to an interface of a Fractal component.
 * More precisely, it gives access to the "functional" interface provided by
 * a server interface, and also gives access to the {@link Interface} interface
 * A stub is also a {@link Name}.
 */

public class UnicastStub implements InvocationHandler, Interface {
  
  protected static Map<String, List<Connection>> connections = new HashMap<String, List<Connection>>();
  
  protected String name = null;
  
  protected Component owner = null;
  
  protected Type type = null;
  
  protected Boolean isInternal = null;
 
  protected TcpIpName id = null;
  
  public UnicastStub(Name name){
    id = (TcpIpName)name;
  }
  
  protected Connection getConnection() throws UnknownHostException, IOException{
    String key = id.getHost()+id.getPort();
    Connection c;
    synchronized(connections){
      if(!connections.containsKey(key)){
        connections.put(key, new LinkedList<Connection>());
      }
      List<Connection> cnxList = connections.get(key);
      if(cnxList.isEmpty()){
        //System.out.println("[stub] create connection for "+host+":"+port);
        c = new Connection();
        c.s = new Socket(id.getHost(),id.getPort());
        c.oos = new RmiObjectOutputStream(c.s.getOutputStream(), id.getNaming());
        c.ois = new RmiObjectInputStream(c.s.getInputStream(), id.getNaming());
      }else{
        //System.out.println("[stub] reuse connection for "+host+":"+port);
        c = cnxList.get(0);
        cnxList.remove(0);
      }
    }
    return c;
  }
  
  protected void releaseConnection(Connection c){
    synchronized(connections){
      connections.get(id.getHost()+id.getPort()).add(c);
    }
  }
  // --------------------------------------------------------------------------
  // InvocationHandler
  // --------------------------------------------------------------------------
  
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result = null;
    if(isDelegatedMethod(method)){
      try{
        result = remoteInvoke(method, args);
      }catch(Throwable e){
        result = e;
      }
    }else{
      try{
        result = method.invoke(this, args);
      }catch(Throwable e){
        result = e;
      }
    }
    if (result == null) {
      return null;
    }else{
      if(result instanceof Throwable){
        if (result instanceof InvocationTargetException){
          throw ((InvocationTargetException)result).getTargetException();
        }else if (result instanceof UndeclaredThrowableException){
          throw ((UndeclaredThrowableException)result).getUndeclaredThrowable();
        }else{
          throw (Throwable)result;
        }
      }else{
        return result;
      }
    }
  }

  protected boolean isDelegatedMethod(Method method) {
    try {
      getClass().getMethod(method.getName(), method.getParameterTypes());
      return false;
    } catch (NoSuchMethodException e) {
      return true;
    }
  }
  
  protected Object remoteInvoke(Method method, Object[] args) throws Throwable{
    Connection c = getConnection();
    Object result = null;
    RpcRequest rr = new RpcRequest(id.getItfId(), method.getName(), method.getParameterTypes(), args);
    //System.out.println("[stub]"+itfId+":"+host+":"+port+" - "+method.toGenericString());
    c.oos.writeObject(rr);
    result = c.ois.readObject();
    releaseConnection(c);
    return result;
  }
  // --------------------------------------------------------------------------
  // Overriden methods
  // --------------------------------------------------------------------------

  /**
   * Tests if the given object is equal to this stub.
   *
   * @param o the object to be compared to this stub.
   * @return <tt>true</tt> if the given object is a stub whose {@link #id id} is
   *      equal to the {@link #id id} of this stub, <tt>false</tt> otherwise.
   */
  @Override
  public boolean equals(final Object o){
    if (o instanceof Interface) {
      return id.getItfId()==((Interface)o).getFcItfGUId();
    }else{
      return false;
    }
  }

  @Override
  public int hashCode(){
    return new Long(id.getItfId()).intValue();
  }
  
  @Override
  public void finalize(){
//    try{
//      ss.close();
//    }catch(IOException ignored){}
  }
  
  //--------------------------------------------------------------------------
  // Interface methods
  // --------------------------------------------------------------------------

  @Override
  public long getFcItfGUId() {
    return id.getItfId();
  }

  @Override
  public String getFcItfName() {
    if(name == null){
      try {
        Method m = Interface.class.getDeclaredMethod("getFcItfName",new Class<?>[]{});
        name = (String)remoteInvoke(m, new Object[]{});
      } catch (Throwable e) {
        e.printStackTrace();
      }
    }
    return name;
  }

  @Override
  public Component getFcItfOwner() {
    if(owner==null){
      try {
        Method m = Interface.class.getDeclaredMethod("getFcItfOwner",new Class<?>[]{});
        owner = (Component)remoteInvoke(m, new Object[]{});
      } catch (Throwable e) {
        e.printStackTrace();
      }
    }
    return owner;
  }

  @Override
  public Type getFcItfType() {
    if(type == null){
      try {
        Method m = Interface.class.getDeclaredMethod("getFcItfType",new Class<?>[]{});
        type = (Type)remoteInvoke(m, new Object[]{});
      } catch (Throwable e) {
        e.printStackTrace();
      }
    }
    return type;
  }

  @Override
  public boolean isFcInternalItf() {
    if(isInternal == null){
      try {
        Method m = Interface.class.getDeclaredMethod("isFcInternalItf",new Class<?>[]{});
        isInternal = (Boolean)remoteInvoke(m, new Object[]{});
      } catch (Throwable e) {
        e.printStackTrace();
      }
    }
    return isInternal;
  }
  
  protected class Connection{
    protected Socket s;
    protected ObjectOutputStream oos;
    protected ObjectInputStream ois;
    
    @Override
    public void finalize(){
      try{
        oos.writeObject(AdminMessage.SHUTDOWN);
        s.close();
      }catch(IOException ignored){}
    }
  }
}
